const vscode = require("vscode");
const { EventEmitter } = require('events');
const { TextEncoder } = require('util');
const childProcess = require('child_process');
const stream = require('stream');
const async = require('async');
const parseGdbMiOut = require('gdb-mi-parser');
const {Gdb} = require('./gdb');
const { Utils } = require('../utils');
let myfunction = new Utils;

function timeout(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

class RuntimeVariable {
    constructor(name, value) {
        this.name = name;
        this._value = value;
        this._memory = undefined;
    }

    get value() {
        return this._value;
    }

    set value(value) {
        this._value = value;
        this._memory = undefined;
    }

    get memory() {
        if (this._memory === undefined && typeof this._value === 'string') {
            this._memory = new TextEncoder().encode(this._value);
        }
        return this._memory;
    }

    setMemory(data, offset = 0) {
        const memory = this.memory;
        if (!memory) {
            return;
        }

        memory.set(data, offset);
        this._memory = memory;
        this._value = new TextDecoder().decode(memory);
    }
}

class MockRuntime extends EventEmitter {
    constructor(fileAccessor, parent) {
        super();
        this.flag = false;
        this.session = parent;
        this.fileAccessor = fileAccessor;
        this._sourceFile = '';
        this.variables = new Map();
        this.sourceLines = [];
        this.instructions = [];
        this.starts = [];
        this.ends = [];
        this._currentLine = 0;
        this.instruction = 0;
        this._breakPoints = new Map();
        this.instruction_breakPoints = new Set();
        this._breakpointId = 1;
        this.breakAddresses = new Map();
        this.namedException = undefined;
        this.otherExceptions = false;
        this.running = false;
        this.gdb = new Gdb();
        this.stop_thread_id = 1;
        this.gdb.on('dataStream', (data) => {
            if (this.rawDisabled) {
                this.rawDisabled = false;
            } else {
                this.sendEvent('outputRaw', data);
            }
        });

        this.gdb.on('breakpointModified', (record) => {
            this.verifyBreakpoint(record);
        });

        this.gdb.on('stopped', (record) => {
            this.stop_thread_id = record.result['thread-id'];
            switch (record.result.reason) {
                case 'breakpoint-hit':
                    this.sendEvent('stopOnBreakpoint', this.stop_thread_id);
                    break;
                case 'syscall-entry':
                case 'end-stepping-range':
                        this.sendEvent('stopOnStep', this.stop_thread_id);
                    break;
                case 'exited-signalled':
                case 'exited-normally':
                    this.sendEvent('end', this.stop_thread_id);
                    break;
                case 'signal-received':
                    this.lastExceptionResult = record.result;
                    this.sendEvent('stopOnException', "Received signal");
                    break;
            }
        });

        this.gdb.on('end', () => {
            setTimeout(() => {
                this.sendEvent('end');
            }, 1000);
        });
    }
    get sourceFile() {
        return this._sourceFile;
    }
    set sourceFile(x) {
        this.loadSource(x);
    }
    get currentLine()
    {
        return this._currentLine;
    }
    set currentLine(x) {
        this._currentLine = x;
        this.instruction = this.starts[x];
    }
    getGdb() {
        return this.gdb;
    }
    isRunning() {
        return this.running;
    }
    async start(program, stopOnEntry, debug) {
        program = program.replace(/\\/g, "/");
        await this.gdb.initRegisters();
        this.gdb.initSendCommand();
        let res = await this.gdb.send(`file ${program}\n`);
        if (res == "No such file or directory.") {
            // 发送结束命令
            this.sendEvent('stopOnException', "Received signal");
        }
        // console.log(this.sourceFile);
        // console.log(this.sourceLines);
        if (stopOnEntry) {
            // let record = await this.gdb.send(`-break-insert main\n`);
            // if (record.resultRecord.class === 'error') {
            //     this._breakpointId--
            // } else {
            //     this.verifyBreakpoint(record.resultRecord)
            // }
        }
        await this.create_breakPoints();

        res = await this.gdb.send(`run\n`);

        let reg = /at\s(.*):(\d+)/;
        let result = res.match(reg);
        let mess = result[1] + ":" + result[2];
        //console.log(mess); // main1.swcu:33
        
        this.running = true;
        this.sendEvent('stopOnBreakpoint', 1);
        return
    }

    async create_breakPoints(path = undefined) {
        let bps = this._breakPoints.get(path);
		if (bps)
			for (const bp of bps) {
                let gdbline = (bp.line) + 1;
                let record = await this.gdb.send(`b ${path}${gdbline ? ':' + (gdbline) : ''}\n`)
                //console.log(record)
                let status = record.indexOf("No source file named");
                if (record.indexOf("No source file named") != -1) {
                    this._breakpointId--;
                } else {
                    this.verifyBreakpoint(record)
                }
            }
		else
			for (let [path, bps] of this._breakPoints) {
				for (const bp of bps) {
                    // 界面给到 gdb  line 要 -1
                    let gdbline = (bp.line) + 1;
					let record = await this.gdb.send(`b ${path}${gdbline ? ':' + (gdbline) : ''}\n`)
                    //console.log(record)
                    let status = record.indexOf("No source file named");
                    if (record.indexOf("No source file named") != -1) {
                        this._breakpointId--;
                    } else {
                        this.verifyBreakpoint(record)
                    }
					// if (record.resultRecord.class === 'error') {
					// 	this._breakpointId--
					// } else {
					// 	this.verifyBreakpoint(record.resultRecord)
					// }
				}
			}
		return
    }

    sendFlag(flag)
    {
        this.flag = flag;
    }

    async continue(reverse) {
        let res = await this.gdb.send(`c\n`)
        if (res.indexOf('exited normally') != -1) {
            this.sendEvent('end');
            await this.gdb.send('quit\n');
        }
        this.sendEvent('stopOnBreakpoint', 1);
    }

    async step(instruction, reverse) {
        await this.gdb.send(`-exec-next\n`);
    }

    updateCurrentLine(reverse) {
        // if (reverse) {
        //     if (this.currentLine > 0) {
        //         this.currentLine--;
        //     } else {
        //         this.currentLine = 0;
        //         this.currentColumn = undefined;
        //         this.sendEvent('stopOnEntry');
        //         return true;
        //     }
        // } else {
        //     if (this.currentLine < this.sourceLines.length - 1) {
        //         this.currentLine++;
        //     } else {
        //         this.currentColumn = undefined;
        //         this.sendEvent('end');
        //         return true;
        //     }
        // }
        // return false;
    }

    stepIn(targetId) {
        // if (typeof targetId === 'number') {
        //     this.currentColumn = targetId;
        //     this.sendEvent('stopOnStep');
        // } else {
        //     if (typeof this.currentColumn === 'number') {
        //         if (this.currentColumn <= this.sourceLines[this.currentLine].length) {
        //             this.currentColumn += 1;
        //         }
        //     } else {
        //         this.currentColumn = 1;
        //     }
        //     this.sendEvent('stopOnStep');
        // }
        this.sendEvent('stopOnStep');
    }
    
    stepOut() {
        if (typeof this.currentColumn === 'number') {
            this.currentColumn -= 1;
            if (this.currentColumn === 0) {
                this.currentColumn = undefined;
            }
        }
        this.sendEvent('stopOnStep');
    }
    
    getStepInTargets(frameId) {
        const line = this.getLine();
        const words = this.getWords(this.currentLine, line);
    
        if (frameId < 0 || frameId >= words.length) {
            return [];
        }
    
        const { name, index } = words[frameId];
    
        return name.split('').map((c, ix) => {
            return {
                id: index + ix,
                label: `target: ${c}`
            };
        });
    }
    
	async stack(startFrame, endFrame) {
        const frames = new Array();
        // if (startFrame != 0) {
        //     return {
        //         frames: frames,
        //         count: frames.length
        //     };
        // }
        if (this.flag) {
            // let resp = await this.gdb.send(`-stack-list-frames ${startFrame} ${endFrame}\n`);
            // console.log(resp);
            let btStr = await this.gdb.send('bt\n');
            let reg2 = /(\w+\.swcu):(\d+)/g;
            let result = btStr.match(/[^\s]*\s*\(.*\)\sat/g);
            let result2 = btStr.match(reg2);
            if (result == undefined) {
                return {
                    frames: frames,
                    count: frames.length
                };
            }
            for (let i = 0; i < result.length; ++i) {
                frames.push({
                    index: i,
                    name: result[i].slice(0, result[i].length - 3),
                    file: vscode.workspace.workspaceFolders[0].uri.fsPath + "/" + result2[i].split(":")[0],
                    line: parseInt(result2[i].split(":")[1])
                });
            }
            
        } else {
            let btStr = await this.gdb.offlinesend('bt\n');
            let reg2 = /(\w+\.swcu):(\d+)/g;
            let result = btStr.match(/[^\s]*\s*\(.*\)\sat/g);
            let result2 = btStr.match(reg2);
            if (result == undefined) {
                return {
                    frames: frames,
                    count: frames.length
                };
            }
            for (let i = 0; i < result.length; ++i) {
                frames.push({
                    index: i,
                    name: result[i].slice(0, result[i].length - 3),
                    file: vscode.workspace.workspaceFolders[0].uri.fsPath + "/" + result2[i].split(":")[0],
                    line: parseInt(result2[i].split(":")[1])
                });
            }
        }
        return {
            frames: frames,
            count: frames.length
        };
	}
    
    get_breakPoints(path, line) {
        return this.getWords(line, this.getLine(line)).filter(w => w.name.length > 8).map(w => w.index);
    }
    
    async setBreakPoint(path, line) {
        path = this.normalizePathAndCasing(path);
    
        const bp = { verified: true, line, id: this._breakpointId++ };
        let bps = this._breakPoints.get(path);
        if (!bps) {
            bps = [];
            this._breakPoints.set(path, bps);
        }
        bps.push(bp);
    
        await this.verify_breakPoints(path);
    
        return bp;
    }
    
    async clearBreakPoints(path) {
        console.log(this.isRunning())
        if (this.isRunning() === false) {
            return;
        }
        let record = await this.gdb.send(`i b\n`)
        let count = (record.match(/(\n)(\d+)/g) || []);
        for (let i = 0; i < count.length; i++) {
            await this.gdb.send(`d ${parseInt(count[i])}\n`)
        }
        this._breakPoints.delete(path);
		return;
    }
    
    clear_breakPoints(path) {
        this._breakPoints.delete(this.normalizePathAndCasing(path));
    }
    
    setDataBreakpoint(address, accessType) {
        const x = accessType === 'readWrite' ? 'read write' : accessType;
    
        const t = this.breakAddresses.get(address);
        if (t) {
            if (t !== x) {
                this.breakAddresses.set(address, 'read write');
            }
        } else {
            this.breakAddresses.set(address, x);
        }
        return true;
    }
    
    clearAllData_breakPoints() {
        this.breakAddresses.clear();
    }
    
    setExceptionsFilters(namedException, otherExceptions) {
        this.namedException = namedException;
        this.otherExceptions = otherExceptions;
    }
    
    setInstructionBreakpoint(address) {
        this.instruction_breakPoints.add(address);
        return true;
    }
    
    clearInstruction_breakPoints() {
        this.instruction_breakPoints.clear();
    }
    
    async getGlobalVariables(cancellationToken) {
        let a = [];
    
        for (let i = 0; i < 10; i++) {
            a.push(new RuntimeVariable(`global_${i}`, i));
            if (cancellationToken && cancellationToken()) {
                break;
            }
            await timeout(1000);
        }
    
        return a;
    }
    
    getLocalVariables() {
        return Array.from(this.variables, ([name, value]) => value);
    }
    
    getLocalVariable(name) {
        return this.variables.get(name);
    }
    
    disassemble(address, instructionCount) {
        const instructions = [];
    
        for (let a = address; a < address + instructionCount; a++) {
            if (a >= 0 && a < this.instructions.length) {
                instructions.push({
                    address: a,
                    instruction: this.instructions[a].name,
                    line: this.instructions[a].line
                });
            } else {
                instructions.push({
                    address: a,
                    instruction: 'nop'
                });
            }
        }
    
        return instructions;
    }
    
    getLine(line) {
        // if (line === undefined) {
        //     line = this.currentLine;
        // }
        // if (line >= this    .sourceLines.length) {
        //     return "";
        // }
        // return this.sourceLines[line === undefined ? this.currentLine : line].trim();
    }
    
    getWords(l, line) {
        const WORD_REGEXP = /[a-z]+/ig;
        const words = [];
        let match;
        while (match = WORD_REGEXP.exec(line)) {
            words.push({ name: match[0], line: l, index: match.index });
        }
        return words;
    }
    
    async loadSource(file) {
        if (this._sourceFile !== file) {
            this._sourceFile = this.normalizePathAndCasing(file);
            this.initializeContents(await this.fileAccessor.readFile(file));
        }
    }
    
    initializeContents(memory) {
        this.sourceLines = new TextDecoder().decode(memory).split(/\r?\n/);
    
        this.instructions = [];
    
        this.starts = [];
        this.instructions = [];
        this.ends = [];
    
        for (let l = 0; l < this.sourceLines.length; l++) {
            this.starts.push(this.instructions.length);
            const words = this.getWords(l, this.sourceLines[l]);
            for (let word of words) {
                this.instructions.push(word);
            }
            this.ends.push(this.instructions.length);
        }
    }
    
    findNextStatement(reverse, stepEvent) {
        // for (let ln = this.currentLine; reverse ? ln >= 0 : ln < this.sourceLines.length; reverse ? ln-- : ln++) {
        //     const _breakPoints = this._breakPoints.get(this._sourceFile);
        //     if (_breakPoints) {
        //         const bps = _breakPoints.filter(bp => bp.line === ln);
        //         if (bps.length > 0) {
        //             this.sendEvent('stopOnBreakpoint');
        //             if (!bps[0].verified) {
        //                 bps[0].verified = true;
        //                 this.sendEvent('breakpointValidated', bps[0]);
        //             }
    
        //             this.currentLine = ln;
        //             return true;
        //         }
        //     }
    
        //     const line = this.getLine(ln);
        //     if (line.length > 0) {
        //         this.currentLine = ln;
        //         break;
        //     }
        // }
        // if (stepEvent) {
        //     this.sendEvent(stepEvent);
        //     return true;
        // }
        // return false;
    }
    
    executeLine(ln, reverse) {
        while (reverse ? this.instruction >= this.starts[ln] : this.instruction < this.ends[ln]) {
            reverse ? this.instruction-- : this.instruction++;
            if (this.instruction_breakPoints.has(this.instruction)) {
                this.sendEvent('stopOnInstructionBreakpoint');
                return true;
            }
        }
    
        const line = this.getLine(ln);
    
        let reg0 = /\$([a-z][a-z0-9]*)(=(false|true|[0-9]+(\.[0-9]+)?|\".*\"|\{.*\}))?/ig;
        let matches0;
        while (matches0 = reg0.exec(line)) {
            if (matches0.length === 5) {
                let access;
    
                const name = matches0[1];
                const value = matches0[3];
    
                let v = new RuntimeVariable(name, value);
    
                if (value && value.length > 0) {
                    if (value === 'true') {
                        v.value = true;
                    } else if (value === 'false') {
                        v.value = false;
                    } else if (value[0] === '"') {
                        v.value = value.slice(1, -1);
                    } else if (value[0] === '{') {
                        v.value = [
                            new RuntimeVariable('fBool', true),
                            new RuntimeVariable('fInteger', 123),
                            new RuntimeVariable('fString', 'hello'),
                            new RuntimeVariable('flazyInteger', 321)
                        ];
                    } else {
                        v.value = parseFloat(value);
                    }
    
                    if (this.variables.has(name)) {
                        access = 'write';
                    }
                    this.variables.set(name, v);
                } else {
                    if (this.variables.has(name)) {
                        access = 'read';
                    }
                }
    
                const accessType = this.breakAddresses.get(name);
                if (access && accessType && accessType.indexOf(access) >= 0) {
                    this.sendEvent('stopOnDataBreakpoint', access);
                    return true;
                }
            }
        }
    
        const reg1 = /(log|prio|out|err)\(([^\)]*)\)/g;
        let matches1;
        while (matches1 = reg1.exec(line)) {
            if (matches1.length === 3) {
                this.sendEvent('output', matches1[1], matches1[2], this._sourceFile, ln, matches1.index);
            }
        }
    
        const matches2 = /exception\((.*)\)/.exec(line);
        if (matches2 && matches2.length === 2) {
            const exception = matches2[1].trim();
            if (this.namedException === exception) {
                this.sendEvent('stopOnException', exception);
                return true;
            } else {
                if (this.otherExceptions) {
                    this.sendEvent('stopOnException', undefined);
                    return true;
                }
            }
        } else {
            if (line.indexOf('exception') >= 0) {
                if (this.otherExceptions) {
                    this.sendEvent('stopOnException', undefined);
                    return true;
                }
            }
        }
        return false;
    }

    verifyBreakpoint(record) {
        // let result1 = record.match(/Breakpoint (\d+)/);
		// let result2;
        // result2 = record.match(/line (\d+)/);
        // if (result2 == undefined) {
        //     let reg = /(\w+\.swcu):(\d+)/g;
        //     let str = record.match(reg);
        //     result2 = str[0].split(":");
        // }
		// //console.log(result1[1], result2[1]); // 1 33
		// setTimeout(() => {
		// 	this.sendEvent('breakpointValidated', {
		// 		verified: true,
		// 		line: parseInt(result1[1]),
		// 		id: parseInt(result2[1])
		// 	});
		// }, 250);
	}
    
    async verify_breakPoints(path) {
        // const bps = this._breakPoints.get(path);
        // if (bps) {
        //     await this.loadSource(path);
        //     bps.forEach(bp => {
        //         if (!bp.verified && bp.line < this.sourceLines.length) {
        //             const srcLine = this.getLine(bp.line);
    
        //             if (srcLine.length === 0 || srcLine.indexOf('+') === 0) {
        //                 bp.line++;
        //             }
        //             if (srcLine.indexOf('-') === 0) {
        //                 bp.line--;
        //             }
        //             if (srcLine.indexOf('lazy') < 0) {
        //                 bp.verified = true;
        //                 this.sendEvent('breakpointValidated', bp);
        //             }
        //         }
        //         bp.verified = true;
        //         this.sendEvent('breakpointValidated', bp);
        //     });
        // }
    }

    sendEvent(event, ... args) {
        setTimeout(() => {
            this.emit(event, ...args);
        }, 0);
    }

    normalizePathAndCasing(path) {
        if (path == "") {
            return "";
        }
        if (this.fileAccessor.isWindows) {
            return path.replace(/\//g, '\\').toLowerCase();
        } else {
            return path.replace(/\\/g, '/');
        }
    }
    async info() {
        const infoStr = await this.gdb.offlinesend('info locals\n');
        const argsStr = await this.gdb.offlinesend('info args\n');

        const variablesInfo = myfunction.infoReg(infoStr);
        const variablesArgs = myfunction.infoReg(argsStr);
        const variables = Object.assign({}, variablesInfo, variablesArgs);
		let getvariables = Object.keys(variables).map(key => ({ name: key, value: variables[key].toString() }));
        return getvariables;
    }
}
    
module.exports = {MockRuntime, RuntimeVariable, timeout}